package wheel;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

import wheel.Computer;
import wheel.Guess;
import wheel.Word;
import wheel.Game.ContestantAction;
import wheel.Game.Category;

/**
 * An AI agent is a subclass of Computer. 
 * 
 * This class overrides selected methods of Computer to implement more
 * intelligent decision making over the default behavior using AI techniques.
 * 
 * @author Jack Myers and Steve Gennaoui
 * @version 1.0
 *
 */
public class AIagent extends Computer {
	
	/* Listed below are configuration parameters for AI agents.
	 * Agents can be created with different thresholds which will adjust their playing style
	 */
	
		// Threshold under which you will try to get more money.
		protected int winningsThreshold; 
		
		// # of total word possibilities under which you should not guess
	    protected int totalWordPossibilityThreshold; 
	
	    // # of total word possibilities over which standard guessing order is used
	    protected int letterAnalysisThreshold; 

	    // How certain we would like to be that a given letter shows up in a given location
	    // as a percentage (ie.. 90 = 90% certain)
	    protected int letterAtLocationThreshold;
	    
	    // The minimum number of word possibilities to guess for each word.
	    // Databases are searched in order to bring back a number of possibilities per
	    // word.  When should we declare we have "enough" words, and stop "lower value"
	    // database searches?
	    protected int perWordPossibilityThreshold;

    /* private instance variables used across various methods. */

	    // A list of all the letters contained in all the word possibilities
	    private String lettersInPossibleWords = ""; 

	    // Special group of pluralized categories where the letter "S" is a very likely guess
		private ArrayList<Category> likelySCategories = new ArrayList<Category>(
			Arrays.asList(			
			   Category.CHARACTERS,
			   Category.EVENTS,
			   Category.LIVING_THINGS,
			   Category.PEOPLE,
			   Category.PROPER_NAMES,
			   Category.THINGS
			   )
		);
		
	    // Special group of pluralized categories where the word "and" is a very likely guess
		private ArrayList<Category> likelyANDCategories = new ArrayList<Category>(
				Arrays.asList(			
				   Category.CHARACTERS,
				   Category.EVENTS,
				   Category.LIVING_THINGS,
				   Category.OCCUPATIONS,
				   Category.PEOPLE,
				   Category.PLACES,
				   Category.PROPER_NAMES,
				   Category.SONG_TITLES,
				   Category.THINGS,
				   Category.TITLES,
				   Category.TV_TITLES
				   )
			);
	
	/* Constructors */
	
	public AIagent(String name, boolean debugMode) {
		super(name, debugMode);
	}
	
	public AIagent(String name, boolean debugMode, int winningsThreshold, 
					int totalWordPossibilityThreshold, int perWordPossibilityThreshold,
					int letterAnalysisThreshold, int letterAtLocationThreshold) {
		super(name, debugMode);
		this.winningsThreshold = winningsThreshold;
		this.totalWordPossibilityThreshold = totalWordPossibilityThreshold;
		this.perWordPossibilityThreshold = perWordPossibilityThreshold;
		this.letterAnalysisThreshold = letterAnalysisThreshold;
		this.letterAtLocationThreshold = letterAtLocationThreshold;
	}

	/* Behavior overrides */
	
	@Override
	public ContestantAction getContestantAction() {
		/* Follow this logic:
		 * 
		 * 1.  	First priority is to make "enough" money, as defined by winningsThreshold, 
		 *     	so obviously this means spin the wheel.
		 *     
		 * 2.  	Guess vowels, so long as the $250 penalty does not lower winnings below
		 * 		threshold.
		 * 
		 * 3.	Guess consonants until the number of possible word guesses is below the
		 * 		totalWordPossibilityThreshold.
		 * 
		 * 4.	Solve.
		 * 
		 */

		if (this.winnings < this.winningsThreshold && currentPuzzle.isMoreConsonants()) {
			// Agent won't solve until it has the minimum winnings it wanted to win.
			
			System.out.println("\t\"I need to win $" + 
									(this.winningsThreshold - winnings) + ", Pat.\"");
			System.out.println(Computer.SPIN);
			return Game.ContestantAction.SPIN_WHEEL;
		}
		else if (prepareToSolve() & 
				this.totalWordPossibilities > 
					this.totalWordPossibilityThreshold + currentPuzzle.getWords().length) {
			/* Agent prepares to solve and notes that too many possibilities remain.
			 * Note: & (not &&) is used to preclude short circuiting.
			 */
			
			if (winnings >= this.winningsThreshold + 250 && currentPuzzle.isMoreVowels() ) {
				// If Agent can afford to buy vowel, and vowels exist, buy one, else spin..
				System.out.println(Computer.VOWEL);
				return Game.ContestantAction.BUY_VOWEL;
			}
			else if (currentPuzzle.isMoreConsonants()) {
				System.out.println(Computer.SPIN);
				return Game.ContestantAction.SPIN_WHEEL;
			}
			else {
				System.out.println(Computer.SOLVE);
				return Game.ContestantAction.SOLVE;
			}
		}
		else {
			// Solve puzzle if possible
			prepareToSolve();
			if (!formulateSolution().equals(NO_SOLUTION)) {
				System.out.println(Computer.SOLVE);
				return Game.ContestantAction.SOLVE;
			}
			else {
				// I have no solution, must resort to picking letters
				if (currentPuzzle.isMoreConsonants()) {
					System.out.println(Computer.SPIN);
					return Game.ContestantAction.SPIN_WHEEL;
				}
				else if (currentPuzzle.isMoreVowels() && winnings >= 250) {
					System.out.println(Computer.VOWEL);
					return Game.ContestantAction.BUY_VOWEL;
				}
				else {
					// No more consonants, and not enough money for vowel....in trouble!
					return Game.ContestantAction.WAVE_TO_VANNA;  // Nothing else to do
				}			
			}
		}
	}


	@Override
	public ContestantAction getFreePlayAction() {
		if (this.currentPuzzle.isMoreVowels()) {
			System.out.println("\t\"I will take a free vowel.\"");
			return Game.ContestantAction.FREE_VOWEL;
		}
		else if (this.currentPuzzle.isMoreConsonants()) {
			System.out.println("\t\"I will take a free consonant.\"");
			return Game.ContestantAction.FREE_CONSONANT;
		}
		else {
			System.out.println("\t\"I'd like a free chance to solve the puzzle.\"");
			return Game.ContestantAction.FREE_GUESS;
		}
	}
	

	/**
	 * updateWordGuesses queries the database and loads possible guesses for each
	 * word into each Word's ArrayList of guesses.  This version takes advantage
	 * of learned words.
	 */

    
    @Override
    protected void updateWordGuesses() {

        String query; 
        this.totalWordPossibilities = 0;  // Reset the total number of possibilities
		lettersInPossibleWords = "";      // Reinitialize
        int i = 0;                        // Counter for the Words in the array of Words
		
		LinkedHashMap<String, Guess> guesses = new LinkedHashMap<String, Guess>();
		
        for (Word myWord : this.words) {
  		
    		if (!myWord.getSolved()) {
    			
    			// Add learned words for the category which are also popular_words
    			query =  "select distinct word from learned_words ";
    			query += "       join popular_words using (word) ";
        		query += "where word regexp '" + myWord.getPattern() + "' and ";
    			query += "      category = '" + currentPuzzle.getPhrase().getCategory() + "' ";
    			query += "order by popular_words.rank * learned_words.frequency ";
    			
    			guesses = queryDatabase("Learned Words for Category which are also Popular Words",
    					Computer.WordSource.LW_CAT_AND_PW, myWord, query, guesses); 
    			
/*    			// Add learned words for ANY category which are also popular_words
    			if (guesses.size() < this.perWordPossibilityThreshold) {
    							
	    			query =  "select distinct word from learned_words_frequency ";
	    			query += "       join popular_words using (word) ";
	        		query += "where word regexp '" + myWord.getPattern() + "' and ";
	    			query += "      word not in ";
	    			query += "      	(select word from learned_words ";
	    			query += "           where category = '";
	    			query += 					 currentPuzzle.getPhrase().getCategory() + "') ";
	    			query += "order by popular_words.rank * learned_words_frequency.total_frequency";
	    			
	    			guesses = queryDatabase("Learned Words for ANY Category which are also Popular Words",
	    					Computer.WordSource.LW_OTHER_AND_PW, myWord, query, guesses);   
    			}
    			*/
    			
    			// Add learned words for ANY category which are also popular_words
    			if (guesses.size() < this.perWordPossibilityThreshold) {

    				query =  "SELECT distinct table1.word  ";
    				query += "FROM   (select distinct word,'";
    				query += 		         currentPuzzle.getPhrase().getCategory() + "'" ;
    				query += "		         as category,";
    				query += "		         popular_words.rank * learned_words_frequency.total_frequency ";
    				query += "               as sort_order ";
    				query += "		  from   learned_words_frequency ";
    				query += "		         join popular_words using (word) "; 				
	        		query += "		  where  word regexp '" + myWord.getPattern() + "'";
	        		query += "		 ) as table1";
	        		query += "		 LEFT JOIN ";
	        		query += "		 (select word, category from learned_words ";
	        		query += "		  where  word regexp '" + myWord.getPattern() + "' and ";
	        		query += " 		         category = '" + currentPuzzle.getPhrase().getCategory() + "' ";	 
	        		query += "		  ) as table2 ";
	        		query += " 		 ON binary table1.category = table2.category and ";
	        		query += "          table1.word = table2.word ";
	        		query += "WHERE table2.word is null ";
	        		query += "ORDER BY table1.sort_order";	
	        		// Subquery performance is bad in MySQL, converted to more complex but faster JOIN
	        		   			
	    			guesses = queryDatabase("Learned Words for OTHER Categories which are also Popular Words",
	    					Computer.WordSource.LW_OTHER_AND_PW, myWord, query, guesses);  
    			}
    			
    			// Add learned words for the category which are not popular_words
    			if (guesses.size() < this.perWordPossibilityThreshold) {
	    			query =  "select word from learned_words ";
	    			query += "       left join popular_words using (word) ";
	        		query += "where word regexp '" + myWord.getPattern() + "' and ";
	    			query += "      category = '" + currentPuzzle.getPhrase().getCategory() + "' and ";
	    			query += "      popular_words.word is null ";
	    			query += "order by learned_words.frequency";
	    			
	    			guesses = queryDatabase("Learned Words for Category which are not Popular Words",
	    					Computer.WordSource.LW_CAT_NOT_PW, myWord, query, guesses); 
    			}
    			
/*    			// Add learned words for ANY category which not also popular_words
    			if (guesses.size() < this.perWordPossibilityThreshold) {
    							
	    			query =  "select word from learned_words_frequency ";
	    			query += "       left join popular_words using (word) ";
	        		query += "where word regexp '" + myWord.getPattern() + "' and ";
	    			query += "      word not in ";
	    			query += "      	(select word from learned_words ";
	    			query += "           where category = '";
	    			query += 					 currentPuzzle.getPhrase().getCategory() + "') and ";
	    			query += "      popular_words.word is null ";
	    			query += "order by learned_words_frequency.total_frequency";
	    			
	    			guesses = queryDatabase("Learned Words for ANY Category which are not Popular Words",
	    					Computer.WordSource.LW_OTHER_NOT_PW, myWord, query, guesses);   
    			}
*/
    			// Add learned words for ANY category which are also popular_words
    			if (guesses.size() < this.perWordPossibilityThreshold) {

    				query =  "SELECT distinct table1.word  ";
    				query += "FROM   (select distinct word,'";
    				query += 		         currentPuzzle.getPhrase().getCategory() + "'" ;
    				query += "		         as category,";
    				query += "		         popular_words.rank * learned_words_frequency.total_frequency ";
    				query += "               as sort_order ";
    				query += "		  from   learned_words_frequency ";
    				query += "		         left join popular_words using (word) "; 				
	        		query += "		  where  word regexp '" + myWord.getPattern() + "' and ";
	        		query += "               popular_words.word is null";
	        		query += "		 ) as table1";
	        		query += "		 LEFT JOIN ";
	        		query += "		 (select word, category from learned_words ";
	        		query += "		  where  word regexp '" + myWord.getPattern() + "' and ";
	        		query += " 		         category = '" + currentPuzzle.getPhrase().getCategory() + "' ";	 
	        		query += "		  ) as table2 ";
	        		query += " 		 ON binary table1.category = table2.category and ";
	        		query += "          table1.word = table2.word ";
	        		query += "WHERE table2.word is null ";
	        		query += "ORDER BY table1.sort_order";
	        		// Subquery performance is bad in MySQL, converted to more complex but faster JOIN

	        		   			
	    			guesses = queryDatabase("Learned Words for OTHER Categories which are not Popular Words",
	    					Computer.WordSource.LW_OTHER_NOT_PW, myWord, query, guesses);  
    			}

    			
    			
    			// Add popular words 
    			if (guesses.size() < this.perWordPossibilityThreshold) {
	        		query =  "select distinct word from popular_words ";
	        		query += "where word regexp '" + myWord.getPattern() + "' ";
	        		query += "order by rank";
	
	        		guesses = queryDatabase("Popular Words", Computer.WordSource.POPULAR_WORDS,
	        				myWord, query, guesses);
    			}
    			
	            // Add words from the wordnet30.word table
        		if (guesses.size() < this.perWordPossibilityThreshold) {

	        		query =  "select word from wordnet30.word ";
	        		query += "where word regexp '" + myWord.getPattern() + "' "; 
	        		
	        		guesses.putAll(queryDatabase("Wordnet", Computer.WordSource.WORDNET,
	        				myWord, query, guesses));
        		}
        		          
	            // Add words from the Linux table
        		if (guesses.size() < this.perWordPossibilityThreshold) {

	        		query =  "select word from linux_words ";
	        		query += "where word regexp '" + myWord.getPattern() + "' "; 
	        		
	        		guesses.putAll(queryDatabase("Linux Words", Computer.WordSource.LINUX_WORDS,
	        				myWord, query, guesses));
        		}
	            
	            // Add invoke_words as a last resort.
	            
        		if (guesses.size() == 0) {

	        		query =  "select word from invoke_words ";
	        		query += "where word regexp '" + myWord.getPattern() + "' ";     

	        		guesses.putAll(queryDatabase("Invoke Words", 
	        				Computer.WordSource.INVOKE_WORDS, myWord, query, guesses));
	            }
        		
        		
	            // Assign the LinkedHashMap guesses to the word of the puzzle
	            this.words.get(i++).setGuesses(guesses);
	            guesses.clear();
    		}
    		else {
    			// The word is solved, the Computer need not guess.
    			LinkedHashMap<String, Guess> lhm = new LinkedHashMap<String, Guess>();
    			lhm.put(myWord.getText(), new Guess(myWord.getText()));
    			this.words.get(i++).setGuesses(lhm);
    		}
        }
	}
    
    /**
     * 
     */
    private LinkedHashMap<String, Guess> queryDatabase(String queryName, 
    			Computer.WordSource wordSource, Word myWord, String query, 
    			LinkedHashMap<String, Guess> guesses) {
		ResultSet rs = null;
        Statement statement;
       
		if (this.debugMode) {
    		/*System.out.println();
			System.out.println(guesses.size() + " possibilities for " + myWord.getText());
			for (String s : guesses) System.out.print(s + " "); */
    		System.out.println();
			System.out.print("*** Querying " + queryName + "...");
		}	
		try {
			statement = myConnection.createStatement();
			rs = statement.executeQuery(query);
	        while (rs.next() && guesses.size() < this.perWordPossibilityThreshold) {
            	// Check to see if word has been added to map
            	if (!guesses.containsKey(rs.getString("word"))) {
	            	Guess myGuess = new Guess(rs.getString("word"), wordSource);    
	                guesses.put(rs.getString("word"), myGuess);  // First time in, a put will work
	                this.totalWordPossibilities++;
		            this.lettersInPossibleWords += rs.getString("word").toUpperCase();
            	}
            	else {
            		Guess previousGuess = guesses.get(rs.getString("word"));
            		previousGuess.getSources().add(wordSource);	            		
            	}
            }
            if (this.debugMode) {
            	System.out.println("done."); 
    			System.out.println(guesses.size() + " possibilities for " + myWord.getText());
    			for (Entry<String, Guess> entry : myWord.getGuesses().entrySet()) {
    				System.out.print(entry.getKey().toLowerCase() + " ");
    			}
            }
			
		} catch (SQLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if (this.debugMode) displayGuesses(guesses);
		return guesses;
    }
    @Override
	/**
	 * getNextConsonant overrides parent method to add more intelligence to 
	 * consonant guessing.  Specifically, it will guess an "D" or "N" if it
	 * believes the word AND could appear.  It will guess "S" if it suspects
	 * likely pluralization.  At a certain point (configurable based on
	 * letterAnalysisThreshold), it will abandon its default behavior of guessing
	 * consonants in order by general frequency, and will instead guess 
	 * consonants in order by frequency of the remaining possible word guesses.
	 * 
	 * ****** IMPROVEMENTS YET TO BE MADE
	 * ******   1. HAVE IT CHECK EACH WORD FOR A "GUARANTEED LETTER", RATHER
	 * ******	   THAN USING THE COMPLETE SET OF LETTERS ACROSS WORDS
	 * ******	2. ONLY TRY FOR "N" AND "D" IF THE PUZZLE HAS A THREE LETTER
	 * ******      WORD NOT AT THE BEGINNING OR END OF THE PUZZLE THAT MATCHES
	 * ******      THE PATTERN.  (E.G. "A**" OR "***" OR "*N*" 
	 * 
	 * @return		the best consonant
	 */
	protected char getNextConsonant() {
    	
		/* ***********************************************************************************
    	 * 1.  First use knowledge of the categories to make a likely guess
		 * ***********************************************************************************/    	
		if (likelySCategories.contains(currentPuzzle.getPhrase().getCategory()) &&
				!currentPuzzle.getGuesses().contains("S")) {
			System.out.println("\t\"This category is going to influence my consonant guess.\"");
			return 'S';				
		}
		else if (likelyANDCategories.contains(currentPuzzle.getPhrase().getCategory())) {
			if (!currentPuzzle.getGuesses().contains("N")) {
				System.out.println("\t\"This category is going to influence my consonant guess.\"");
				return 'N';
			} 
			else if (!currentPuzzle.getGuesses().contains("D")) {
				System.out.println("\t\"This category is going to influence my consonant guess.\"");
				return 'D';
			}
		}
		else if (currentPuzzle.getPhrase().getCategory() == Game.Category.WHAT_ARE_YOU_DOING) {
			if (!currentPuzzle.getGuesses().contains("N")) {
				System.out.println("\t\"This category is going to influence my consonant guess.\"");
				return 'N';
			} 
			else if (!currentPuzzle.getGuesses().contains("G")) {
				System.out.println("\t\"This category is going to influence my consonant guess.\"");
				return 'G';
			}
		}
    	
		/* ***********************************************************************************
		 *  2.  Examine the word choices to see if there is a position on the puzzle with a 
		 *      near certain letter. 
		 * ***********************************************************************************/
		
    	// Should be a hashmap of size one
    	HashMap<Character, Float> mostCertainAndCertainty = getMostCertainConsonant();
   		if (!mostCertainAndCertainty.keySet().isEmpty()) {
   			// The keyset can be empty if no consonant found
   			// Keyset should be size 1 if not empty
   			if(mostCertainAndCertainty.keySet().size() == 1) {
   				for(Character c: mostCertainAndCertainty.keySet())
   					if(mostCertainAndCertainty.get(c) > letterAtLocationThreshold) {
   						System.out.println("\t\"I am going to guess based on an analysis of words\"");
  						return c;  						
   					}
   			}
   			else {
   	    		System.err.println("getMostCertainConsonant returned HashMap of size != 1");
   	    	}
   		}
		
   		/* ***********************************************************************************
		 *  3.  If there is no certain letter at a particular position, leverage the 
   		 *      pool of word choices to select most frequently occurring letter
		 * ***********************************************************************************/ 		
   		
		if (this.totalWordPossibilities < this.letterAnalysisThreshold) {
			// For a small enough sample, use the returned words to suggest the
			// next consonant.
			//
			System.out.println("\t\"Not many possibilities are left. I will choose a probable letter.\"");
			return getMostFrequentConsonant();
		}

   		/* ***********************************************************************************
		 *  4.  Return to default behavior, based on overall frequency in English language.
		 * ***********************************************************************************/ 
		
    	for (char c : this.consonantsInOrder.toCharArray()) {
    		if (this.currentPuzzle.getGuesses().indexOf(c) == -1) {
    			System.out.println("\t\"I should probably just guess a more common letter.\"");
				return c;
			}
		}
		return 0;
	}

    protected HashMap<Character, Float> getMostCertainConsonant() {
    	return getMostCertainLetter(true);
    }
    
    protected HashMap<Character, Float> getMostCertainVowel() {
    	return getMostCertainLetter(false);
    }
    /**
     * Find a letter that shows up in most guesses for any given position in the solution.
     * 
     * @return	The most certain letter, if it was above the letterAtLocationThreshold as well. 
     * 			Else, returns null
     */
    protected HashMap<Character,Float> getMostCertainLetter(boolean consonantWanted) {
    	Character mostCertainLetter = null;
    	float certainty = -1;
    	HashMap<Character,Float> mostCertainLetterAndCertainty = new HashMap<Character, Float>(1);
    	prepareToSolve();
    	// For each (partially solved) word in the puzzle
    	for (Word wordFromPuzzle: this.words) {
    		// If word is solved entirely, skip it
    		//If word has too many possibilities, go to the next
    		if (wordFromPuzzle.getSolved() ||
    			wordFromPuzzle.getGuessCount() > letterAnalysisThreshold) {
    			continue;
    		}
    		// Get all of the guesses for one particular word
    		LinkedHashMap<String, Guess> guessesForWord = wordFromPuzzle.getGuesses();
    		// For each character in that word
    		for (int i = 0; i < wordFromPuzzle.getLength(); i++) {
    			// If this character has already been finalized in the puzzle, jump right to the next character
    			if (wordFromPuzzle.charAlreadySolved(i)) {
    				continue;
    			}
    			// Create a map of character to quantity it appears in the guesses
    			HashMap<Character, Integer> charQuantities = new HashMap<Character, Integer>(26);
    			// For each guess
    			Iterator<String> it = guessesForWord.keySet().iterator();
    			String guess;
    			while (it.hasNext()) {
    				guess = it.next();
     				// Get the character that appears in this guess at the non-finalized position in the word
    				Character currentCharacter = guess.charAt(i);
    				// Check to see if that character is in the map already
    				Integer currentQuantity = charQuantities.get(currentCharacter);
    				// If it's not, change that null to a 0
    				if (currentQuantity == null)
    					currentQuantity = 0;
    				// Since we are looking at this character, increment the frequency count
    				currentQuantity += 1;
    				// Put the new frequency count into the map for this character
    				charQuantities.put(currentCharacter, currentQuantity);
    			}
    			// At this point, we have gone through every guess for a given index in a word in the solution
    			// For each letter in the keyset
    			for(Character c: charQuantities.keySet()) {
    				// Calculate the percentage that the letter appeared in this position
    				int quantity = charQuantities.get(c);
    				float percentageAppeared = quantity/(float)guessesForWord.size()*100;
    				// If it appeared 100% of the time and it is what we wanted (consonant/vowel)
    				if (percentageAppeared >= 100 &&
    					(consonantWanted && Game.isConsonant(c)) ||	(!consonantWanted && Game.isVowel(c))) {
    					System.out.println("\t\"The character " + c + " appears with 100% certainty for one location.");
    					mostCertainLetterAndCertainty.put(c, (float)100);
    					return mostCertainLetterAndCertainty; // return the 100% character
    				}
    				// If it is over the letterAtLocationThreshold and is what we wanted
    				if ((consonantWanted && Game.isConsonant(c)) ||	(!consonantWanted && Game.isVowel(c))) {
    					// See if it's a more certain choice than the previous one
    					// and if it is update certainty and which is most certain
    					if (percentageAppeared > certainty) {
    						certainty = percentageAppeared;
    						mostCertainLetter = c;
    					}
    				}
    			}
    		}
    	}
    	// If nothing was above the certainty threshold, returns null
    	mostCertainLetterAndCertainty.put(mostCertainLetter, certainty);
    	return mostCertainLetterAndCertainty;
    }

    /**
     * Examines lettersInPossibleWords, and returns the most frequent consonant
     * 
     * @return		most frequent consonant
     */
    // TODO(steve/jack): parameterize me
    protected char getMostFrequentConsonant() {
    	// First examine the word possibilities
    	// TODO(Jack): Do we need to prepare to solve here? Has the puzzle's state changed?
    	// 			   Or are you using this for the side effects of the method? (If this is the case,
    	//			   consider creating a separate method with just those side effects)
    	prepareToSolve();
     	   	
    	// Then strip vowels
    	for (Character c : Game.VOWELS.toCharArray())
    		this.lettersInPossibleWords = 
    			this.lettersInPossibleWords.replace(c.toString(),"");
    	
    	// Then strip guessed letters
    	for (Character c : this.currentPuzzle.getGuesses().toCharArray())
    		this.lettersInPossibleWords = 
    			this.lettersInPossibleWords.replace(c.toString(),"");
    	
    	// Then strip punctuation
    	for (Character c : Game.PUNCTUATION.toCharArray())
    		this.lettersInPossibleWords = 
    			this.lettersInPossibleWords.replace(c.toString(),"");
    	
    	
    	int maxFrequency = 0;
        char mostFrequentConsonant = '~';
              for (int i = 0; i < lettersInPossibleWords.length(); i++)
              {
                  char c = lettersInPossibleWords.charAt(i);
                  int counter = 0;
                  for (int j = lettersInPossibleWords.indexOf(c); j != -1; 
                		  j = lettersInPossibleWords.indexOf(c, j + 1))
                  {
                      counter++;
                  }
                  if (counter > maxFrequency)
                  {
                	  maxFrequency = counter;
                	  mostFrequentConsonant = c;
                  }
              }
    	return mostFrequentConsonant;
    }
    
    @Override
	public void speak() {
		SoundPlayer audio = new SoundPlayer();
		switch (1 + (int)(Math.random() * 3)) {
		case 1:
			audio.playSound(SoundPlayer.SOUND_FILES_DIRECTORY + "RQ1.wav");
			break;
		case 2:
			audio.playSound(SoundPlayer.SOUND_FILES_DIRECTORY + "RQ2.wav");
			break;
		case 3:
			audio.playSound(SoundPlayer.SOUND_FILES_DIRECTORY + "RQ3.wav");
			break;
		}
	}

	@Override
	public void learnPuzzle(Puzzle puzzle) {
		Category category = puzzle.getPhrase().getCategory();
		String[] words = puzzle.getPhrase().getText().split(" ");
		for(String word: words) {
			word = word.toLowerCase();
			String query = "select count(*) as count from learned_words " +
						   "where category='" + category + "' and word='" + word + "'";
			Statement statement;
			String updateQuery;	// the query to update the db and learn
			try {
				statement = myConnection.createStatement();
				ResultSet rs = statement.executeQuery(query);
				// Query database and see if the word already exists for this category
				if (rs.next() && rs.getInt("count") > 0) {
					// increment the count for this word
					updateQuery = "update learned_words set frequency=frequency+1 " +
									"where category='" + category + "' and word='" + word + "'";
				}
				else {
					// add the word/category pair and put count at 1
					//insert into learned_words (word,category,frequency)values('ttyl','AROUND_THE_HOUSE',1);
					updateQuery = "insert into learned_words (word,category,frequency) " +
									"values('" + word + "','" + category + "',1)";
				}
				statement.executeUpdate(updateQuery);
			} catch (SQLException e) {
				System.out.println("AIagent.learnPuzzle: " + e);
			}
            
		}
	}
	
	/**
	 * We are want to use a standard order for consonants at some point.
	 * Since we only populate guesses up to the perWordPossibilityThreshold,
	 * "too many possibilities" (i.e. letterAnalysisThreshold) will be
	 * determined by the number of words in the puzzle * the perWordPossibilityThreshold.
	 */
	@Override
	public void setCurrentPuzzle(Puzzle currentPuzzle) {
		this.currentPuzzle = currentPuzzle;
		loadWords();
		this.regexp = "[abcdefghijklmnopqrstuvwxyz]";
		this.letterAnalysisThreshold = this.words.size() * this.perWordPossibilityThreshold;
	}
	
	/**
	 * for debugging
	 * 
	 * @param guesses
	 */
	private void displayGuesses(LinkedHashMap<String, Guess> guesses) {
		System.out.println("Displaying guesses");
		for (Map.Entry<String, Guess> entry : guesses.entrySet()) {
			Guess g = entry.getValue();
			System.out.println(entry.getKey());
			for (WordSource ws : g.getSources()) {
				System.out.println(ws);
			}
		}
	}

	public void setWinningsThreshold(int winningsThreshold) {
		this.winningsThreshold = winningsThreshold;
	}
	
}
